package net.homeip.jcperera.nu.model.entity;

import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;

import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Named;
import javax.persistence.EntityManager;
import javax.persistence.Query;

import net.homeip.jcperera.nu.core.CoreBase;
import net.homeip.jcperera.nu.core.boot.Boot;
import net.homeip.jcperera.nu.core.msg.BaseMessage;
import net.homeip.jcperera.nu.core.msg.ErrorMessage;
import net.homeip.jcperera.nu.core.msg.Logger;
import net.homeip.jcperera.nu.security.user.User;

/**
 * System Type Manager, initializes the library of available types and maintain the collection for all of them
 * 
 * @name Authenticator class
 * @author julio.c.perera@gmail.com
 * @application nu
 * @since 0.1.0.0
 */

@Stateless
@LocalBean
@TransactionManagement(TransactionManagementType.BEAN)
@Named(TypeManager.componentName)
@ApplicationScoped
public class TypeManager extends CoreBase implements Serializable
{
    // Required to implement Serializable interface
    private static final long           serialVersionUID = -5697992935056778590L;

    // CoreBase properties, for easy use from other components, as well as logging
    public static final int             runtimeLevel     = 1;
    public static final String          moduleName       = "model";
    public static final String          componentName    = "nu_model_entity_type_manager";

    // Get the log
    private final static Logger         log              = Logger.getLogger();

    // Here are the values of the Types as such
    private final HashMap<String, Type> values           = new HashMap<String, Type>();

    /**
     * Initializes this {@link TypeManager} instance
     */
    public void start() throws Exception
    {
        log.info("Starting Type Manager...");
        int prevSize = this.values.size();
        load();
        if (prevSize == 0) log.info("Readed " + this.values.size() + " types from database");
        else log.info("Found already loaded " + prevSize + " types");
        if (this.values.size() == 0)
        {
            log.info("No types found, creating minimal core predefined types...");
            getTypeByClassName(Type.class.getName());
            getTypeByClassName(User.class.getName());
        }
    }

    /**
     * <p>
     * Builds a new instance of the class associated to the specified className using the default constructor.
     * </p>
     * <p>
     * <b>Note:</b> The instance should have a public default constructor (a constructor with no parameters) or a error
     * message is returned
     * </p>
     * 
     * @param className
     *            the className in dot notation (i.e.: <code>'my.company.section.foo.bar'</code>)
     * @see Class#forName(String)
     * @see Class#newInstance()
     * @return a newly allocated instance of the specified class
     */
    public static Object getInstanceByClassName(String className) throws BaseMessage
    {
        Object instance = null;
        try
        {
            Class<?> vclass = Class.forName(className);
            Constructor<?> constructor = vclass.getConstructor();
            instance = constructor.newInstance();

            if (instance == null) { throw new ClassNotFoundException("Requested class not found: " + className); }
        }
        catch (Exception exception)
        {
            Hashtable<String, String> extensions = new Hashtable<String, String>();
            extensions.put("%className%", className);
            extensions.put("%description%", exception.toString());
            throw new ErrorMessage("nu.core.entity.type.instantiation.error", BaseMessage.FRIENDLINESS_TECHNICAL,
                    extensions);
        }
        return instance;
    }

    /**
     * <p>
     * Builds a new instance of the class associated to the specified class using the default constructor.
     * </p>
     * <p>
     * <b>Note:</b> The instance should have a public default constructor (a constructor with no parameters) or a error
     * message is returned
     * </p>
     * 
     * @param class the class associated to the object to build
     * @see Class#newInstance()
     * @return a newly allocated instance of the specified class
     */
    public static Object getInstanceByClass(Class<?> vclass) throws BaseMessage
    {
        Object instance = null;
        try
        {
            Constructor<?> constructor = vclass.getConstructor();
            instance = constructor.newInstance();
            if (instance == null) { throw new ClassNotFoundException("Requested class not found: " + vclass.getName()); }
        }
        catch (Exception exception)
        {
            Hashtable<String, String> extensions = new Hashtable<String, String>();
            extensions.put("%className%", vclass.getName());
            extensions.put("%description%", exception.toString());
            throw new ErrorMessage("nu.core.entity.type.instantiation.error", BaseMessage.FRIENDLINESS_TECHNICAL,
                    extensions);
        }
        return instance;
    }

    /**
     * Gets the type instance associated to an {@link BaseEntity}, in case no instance of type is associated to the
     * {@link BaseEntity} class, a new one is created and stored
     * 
     * @param entity
     *            the {@link BaseEntity} to get the associated {@link Type}
     * @return the corresponding Type Entity
     */
    public Type getTypeByEntity(BaseEntity entity) throws BaseMessage
    {
        return getTypeByClassName(entity.getClass().getName());
    }

    /**
     * Gets the type instance associated to a specified className, in case no instance of type is associated to the
     * className, a new one is created and stored
     * 
     * @param className
     *            the className to get the associated {@link Type}
     * @return the corresponding Type Entity
     */
    public Type getTypeByClassName(String className) throws BaseMessage
    {
        synchronized (this)
        {
            if (this.values.containsKey(className)) return this.values.get(className);
            else
            {
                Type type = (Type) getInstanceByClassName(Type.class.getName());
                type.setClassName(className);
                type.setType(type);
                this.values.put(className, type);
                EntityManager em = Boot.getSystem().getPersistence().getEntityManagerFactory().createEntityManager();
                em.getTransaction().begin();
                em.persist(type);
                em.getTransaction().commit();
                em.close();
                return type;
            }
        }
    }

    /**
     * <p>
     * Loads the data associated to <code>type</code>s from the database, but only if there is no data in the collection
     * yet
     * </p>
     */
    private void load() throws Exception
    {
        if (this.values.size() == 0)
        {
            EntityManager em = Boot.getSystem().getPersistence().getEntityManagerFactory().createEntityManager();
            Query query = em.createQuery("select object(i) from Type i");
            @SuppressWarnings("unchecked")
            List<Type> existing = (List<Type>) query.getResultList();
            em.close();
            for (Type type : existing)
                this.values.put(type.getClassName(), type);
        }
    }

    /**
     * <p>
     * Reloads the data associated to <code>Type</code>s as stored in the database, unconditionally.
     * </p>
     */
    public void reload()
    {
        synchronized (this)
        {
            try
            {
                log.info("Reloading configuration at request");
                values.clear();
                load();
            }
            catch (Exception exception)
            {
                log.error("Error while reloading configuration, cannot continue: '" + exception.getLocalizedMessage()
                        + "'", exception);
            }
        }
    }

    // Destroy
    public void stop()
    {
        synchronized (this)
        {
            this.values.clear();
        }
        log.info("Stopping Type Manager...");
    }
}